/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package com.feilong.lib.javassist.compiler;

import com.feilong.lib.javassist.CannotCompileException;
import com.feilong.lib.javassist.CtBehavior;
import com.feilong.lib.javassist.CtClass;
import com.feilong.lib.javassist.CtConstructor;
import com.feilong.lib.javassist.CtField;
import com.feilong.lib.javassist.CtMember;
import com.feilong.lib.javassist.CtMethod;
import com.feilong.lib.javassist.CtPrimitiveType;
import com.feilong.lib.javassist.Modifier;
import com.feilong.lib.javassist.NotFoundException;
import com.feilong.lib.javassist.bytecode.BadBytecode;
import com.feilong.lib.javassist.bytecode.Bytecode;
import com.feilong.lib.javassist.bytecode.CodeAttribute;
import com.feilong.lib.javassist.bytecode.LocalVariableAttribute;
import com.feilong.lib.javassist.bytecode.Opcode;
import com.feilong.lib.javassist.compiler.ast.ASTList;
import com.feilong.lib.javassist.compiler.ast.ASTree;
import com.feilong.lib.javassist.compiler.ast.CallExpr;
import com.feilong.lib.javassist.compiler.ast.Declarator;
import com.feilong.lib.javassist.compiler.ast.Expr;
import com.feilong.lib.javassist.compiler.ast.FieldDecl;
import com.feilong.lib.javassist.compiler.ast.Member;
import com.feilong.lib.javassist.compiler.ast.MethodDecl;
import com.feilong.lib.javassist.compiler.ast.Stmnt;
import com.feilong.lib.javassist.compiler.ast.Symbol;

public class Javac{

    JvstCodeGen                gen;

    SymbolTable                stable;

    private Bytecode           bytecode;

    public static final String param0Name    = "$0";

    public static final String resultVarName = "$_";

    public static final String proceedName   = "$proceed";

    /**
     * Constructs a compiler.
     *
     * @param thisClass
     *            the class that a compiled method/field
     *            belongs to.
     */
    public Javac(CtClass thisClass){
        this(new Bytecode(thisClass.getClassFile2().getConstPool(), 0, 0), thisClass);
    }

    /**
     * Constructs a compiler.
     * The produced bytecode is stored in the <code>Bytecode</code> object
     * specified by <code>b</code>.
     *
     * @param thisClass
     *            the class that a compiled method/field
     *            belongs to.
     */
    public Javac(Bytecode b, CtClass thisClass){
        gen = new JvstCodeGen(b, thisClass, thisClass.getClassPool());
        stable = new SymbolTable();
        bytecode = b;
    }

    /**
     * Returns the produced bytecode.
     */
    public Bytecode getBytecode(){
        return bytecode;
    }

    /**
     * Compiles a method, constructor, or field declaration
     * to a class.
     * A field declaration can declare only one field.
     *
     * <p>
     * In a method or constructor body, $0, $1, ... and $_
     * are not available.
     *
     * @return a <code>CtMethod</code>, <code>CtConstructor</code>,
     *         or <code>CtField</code> object.
     * @see #recordProceed(String,String)
     */
    public CtMember compile(String src) throws CompileError{
        Parser p = new Parser(new Lex(src));
        ASTList mem = p.parseMember1(stable);
        try{
            if (mem instanceof FieldDecl){
                return compileField((FieldDecl) mem);
            }
            CtBehavior cb = compileMethod(p, (MethodDecl) mem);
            CtClass decl = cb.getDeclaringClass();
            cb.getMethodInfo2().rebuildStackMapIf6(decl.getClassPool(), decl.getClassFile2());
            return cb;
        }catch (BadBytecode bb){
            throw new CompileError(bb.getMessage());
        }catch (CannotCompileException e){
            throw new CompileError(e.getMessage());
        }
    }

    public static class CtFieldWithInit extends CtField{

        private ASTree init;

        CtFieldWithInit(CtClass type, String name, CtClass declaring) throws CannotCompileException{
            super(type, name, declaring);
            init = null;
        }

        protected void setInit(ASTree i){
            init = i;
        }

        @Override
        protected ASTree getInitAST(){
            return init;
        }
    }

    private CtField compileField(FieldDecl fd) throws CompileError,CannotCompileException{
        CtFieldWithInit f;
        Declarator d = fd.getDeclarator();
        f = new CtFieldWithInit(gen.resolver.lookupClass(d), d.getVariable().get(), gen.getThisClass());
        f.setModifiers(MemberResolver.getModifiers(fd.getModifiers()));
        if (fd.getInit() != null){
            f.setInit(fd.getInit());
        }

        return f;
    }

    private CtBehavior compileMethod(Parser p,MethodDecl md) throws CompileError{
        int mod = MemberResolver.getModifiers(md.getModifiers());
        CtClass[] plist = gen.makeParamList(md);
        CtClass[] tlist = gen.makeThrowsList(md);
        recordParams(plist, Modifier.isStatic(mod));
        md = p.parseMethod2(stable, md);
        try{
            if (md.isConstructor()){
                CtConstructor cons = new CtConstructor(plist, gen.getThisClass());
                cons.setModifiers(mod);
                md.accept(gen);
                cons.getMethodInfo().setCodeAttribute(bytecode.toCodeAttribute());
                cons.setExceptionTypes(tlist);
                return cons;
            }
            Declarator r = md.getReturn();
            CtClass rtype = gen.resolver.lookupClass(r);
            recordReturnType(rtype, false);
            CtMethod method = new CtMethod(rtype, r.getVariable().get(), plist, gen.getThisClass());
            method.setModifiers(mod);
            gen.setThisMethod(method);
            md.accept(gen);
            if (md.getBody() != null){
                method.getMethodInfo().setCodeAttribute(bytecode.toCodeAttribute());
            }else{
                method.setModifiers(mod | Modifier.ABSTRACT);
            }

            method.setExceptionTypes(tlist);
            return method;
        }catch (NotFoundException e){
            throw new CompileError(e.toString());
        }
    }

    /**
     * Compiles a method (or constructor) body.
     *
     * @param src
     *            a single statement or a block.
     *            If null, this method produces a body returning zero or null.
     */
    public Bytecode compileBody(CtBehavior method,String src) throws CompileError{
        try{
            int mod = method.getModifiers();
            recordParams(method.getParameterTypes(), Modifier.isStatic(mod));

            CtClass rtype;
            if (method instanceof CtMethod){
                gen.setThisMethod((CtMethod) method);
                rtype = ((CtMethod) method).getReturnType();
            }else{
                rtype = CtClass.voidType;
            }

            recordReturnType(rtype, false);
            boolean isVoid = rtype == CtClass.voidType;

            if (src == null){
                makeDefaultBody(bytecode, rtype);
            }else{
                Parser p = new Parser(new Lex(src));
                SymbolTable stb = new SymbolTable(stable);
                Stmnt s = p.parseStatement(stb);
                if (p.hasMore()){
                    throw new CompileError("the method/constructor body must be surrounded by {}");
                }

                boolean callSuper = false;
                if (method instanceof CtConstructor){
                    callSuper = !((CtConstructor) method).isClassInitializer();
                }

                gen.atMethodBody(s, callSuper, isVoid);
            }

            return bytecode;
        }catch (NotFoundException e){
            throw new CompileError(e.toString());
        }
    }

    private static void makeDefaultBody(Bytecode b,CtClass type){
        int op;
        int value;
        if (type instanceof CtPrimitiveType){
            CtPrimitiveType pt = (CtPrimitiveType) type;
            op = pt.getReturnOp();
            if (op == Opcode.DRETURN){
                value = Opcode.DCONST_0;
            }else if (op == Opcode.FRETURN){
                value = Opcode.FCONST_0;
            }else if (op == Opcode.LRETURN){
                value = Opcode.LCONST_0;
            }else if (op == Opcode.RETURN){
                value = Opcode.NOP;
            }else{
                value = Opcode.ICONST_0;
            }
        }else{
            op = Opcode.ARETURN;
            value = Opcode.ACONST_NULL;
        }

        if (value != Opcode.NOP){
            b.addOpcode(value);
        }

        b.addOpcode(op);
    }

    /**
     * Records local variables available at the specified program counter.
     * If the LocalVariableAttribute is not available, this method does not
     * record any local variable. It only returns false.
     *
     * @param pc
     *            program counter (&gt;= 0)
     * @return false if the CodeAttribute does not include a
     *         LocalVariableAttribute.
     */
    public boolean recordLocalVariables(CodeAttribute ca,int pc) throws CompileError{
        LocalVariableAttribute va = (LocalVariableAttribute) ca.getAttribute(LocalVariableAttribute.tag);
        if (va == null){
            return false;
        }

        int n = va.tableLength();
        for (int i = 0; i < n; ++i){
            int start = va.startPc(i);
            int len = va.codeLength(i);
            if (start <= pc && pc < start + len){
                gen.recordVariable(va.descriptor(i), va.variableName(i), va.index(i), stable);
            }
        }

        return true;
    }

    /**
     * Records parameter names if the LocalVariableAttribute is available.
     * It returns false unless the LocalVariableAttribute is available.
     *
     * @param numOfLocalVars
     *            the number of local variables used
     *            for storing the parameters.
     * @return false if the CodeAttribute does not include a
     *         LocalVariableAttribute.
     */
    public boolean recordParamNames(CodeAttribute ca,int numOfLocalVars) throws CompileError{
        LocalVariableAttribute va = (LocalVariableAttribute) ca.getAttribute(LocalVariableAttribute.tag);
        if (va == null){
            return false;
        }

        int n = va.tableLength();
        for (int i = 0; i < n; ++i){
            int index = va.index(i);
            if (index < numOfLocalVars){
                gen.recordVariable(va.descriptor(i), va.variableName(i), index, stable);
            }
        }

        return true;
    }

    /**
     * Makes variables $0 (this), $1, $2, ..., and $args represent method
     * parameters. $args represents an array of all the parameters.
     * It also makes $$ available as a parameter list of method call.
     *
     * <p>
     * This must be called before calling <code>compileStmnt()</code> and
     * <code>compileExpr()</code>. The correct value of
     * <code>isStatic</code> must be recorded before compilation.
     * <code>maxLocals</code> is updated to include $0,...
     */
    public int recordParams(CtClass[] params,boolean isStatic) throws CompileError{
        return gen.recordParams(params, isStatic, "$", "$args", "$$", stable);
    }

    /**
     * Makes variables $0, $1, $2, ..., and $args represent method
     * parameters. $args represents an array of all the parameters.
     * It also makes $$ available as a parameter list of method call.
     * $0 can represent a local variable other than THIS (variable 0).
     * $class is also made available.
     *
     * <p>
     * This must be called before calling <code>compileStmnt()</code> and
     * <code>compileExpr()</code>. The correct value of
     * <code>isStatic</code> must be recorded before compilation.
     * <code>maxLocals</code> is updated to include $0,...
     *
     * @param use0
     *            true if $0 is used.
     * @param varNo
     *            the register number of $0 (use0 is true)
     *            or $1 (otherwise).
     * @param target
     *            the type of $0 (it can be null if use0 is false).
     *            It is used as the name of the type represented
     *            by $class.
     * @param isStatic
     *            true if the method in which the compiled bytecode
     *            is embedded is static.
     */
    public int recordParams(String target,CtClass[] params,boolean use0,int varNo,boolean isStatic) throws CompileError{
        return gen.recordParams(params, isStatic, "$", "$args", "$$", use0, varNo, target, stable);
    }

    /**
     * Sets <code>maxLocals</code> to <code>max</code>.
     * This method tells the compiler the local variables that have been
     * allocated for the rest of the code. When the compiler needs
     * new local variables, the local variables at the index <code>max</code>,
     * <code>max + 1</code>, ... are assigned.
     *
     * <p>
     * This method is indirectly called by <code>recordParams</code>.
     */
    public void setMaxLocals(int max){
        gen.setMaxLocals(max);
    }

    /**
     * Prepares to use cast $r, $w, $_, and $type.
     * $type is made to represent the specified return type.
     * It also enables to write a return statement with a return value
     * for void method.
     *
     * <p>
     * If the return type is void, ($r) does nothing.
     * The type of $_ is java.lang.Object.
     *
     * @param type
     *            the return type.
     * @param useResultVar
     *            true if $_ is used.
     * @return -1 or the variable index assigned to $_.
     * @see #recordType(CtClass)
     */
    public int recordReturnType(CtClass type,boolean useResultVar) throws CompileError{
        gen.recordType(type);
        return gen.recordReturnType(type, "$r", (useResultVar ? resultVarName : null), stable);
    }

    /**
     * Prepares to use $type. Note that recordReturnType() overwrites
     * the value of $type.
     *
     * @param t
     *            the type represented by $type.
     */
    public void recordType(CtClass t){
        gen.recordType(t);
    }

    /**
     * Makes the given variable available.
     *
     * @param type
     *            variable type
     * @param name
     *            variable name
     */
    public int recordVariable(CtClass type,String name) throws CompileError{
        return gen.recordVariable(type, name, stable);
    }

    /**
     * Prepares to use $proceed().
     * If the return type of $proceed() is void, null is pushed on the
     * stack.
     *
     * @param target
     *            an expression specifying the target object.
     *            if null, "this" is the target.
     * @param method
     *            the method name.
     */
    public void recordProceed(String target,String method) throws CompileError{
        Parser p = new Parser(new Lex(target));
        final ASTree texpr = p.parseExpression(stable);
        final String m = method;

        ProceedHandler h = new ProceedHandler(){

            @Override
            public void doit(JvstCodeGen gen,Bytecode b,ASTList args) throws CompileError{
                ASTree expr = new Member(m);
                if (texpr != null){
                    expr = Expr.make('.', texpr, expr);
                }

                expr = CallExpr.makeCall(expr, args);
                gen.compileExpr(expr);
                gen.addNullIfVoid();
            }

            @Override
            public void setReturnType(JvstTypeChecker check,ASTList args) throws CompileError{
                ASTree expr = new Member(m);
                if (texpr != null){
                    expr = Expr.make('.', texpr, expr);
                }

                expr = CallExpr.makeCall(expr, args);
                expr.accept(check);
                check.addNullIfVoid();
            }
        };

        gen.setProceedHandler(h, proceedName);
    }

    /**
     * Prepares to use $proceed() representing a static method.
     * If the return type of $proceed() is void, null is pushed on the
     * stack.
     *
     * @param targetClass
     *            the fully-qualified dot-separated name
     *            of the class declaring the method.
     * @param method
     *            the method name.
     */
    public void recordStaticProceed(String targetClass,String method) throws CompileError{
        final String c = targetClass;
        final String m = method;

        ProceedHandler h = new ProceedHandler(){

            @Override
            public void doit(JvstCodeGen gen,Bytecode b,ASTList args) throws CompileError{
                Expr expr = Expr.make(TokenId.MEMBER, new Symbol(c), new Member(m));
                expr = CallExpr.makeCall(expr, args);
                gen.compileExpr(expr);
                gen.addNullIfVoid();
            }

            @Override
            public void setReturnType(JvstTypeChecker check,ASTList args) throws CompileError{
                Expr expr = Expr.make(TokenId.MEMBER, new Symbol(c), new Member(m));
                expr = CallExpr.makeCall(expr, args);
                expr.accept(check);
                check.addNullIfVoid();
            }
        };

        gen.setProceedHandler(h, proceedName);
    }

    /**
     * Prepares to use $proceed() representing a private/super's method.
     * If the return type of $proceed() is void, null is pushed on the
     * stack. This method is for methods invoked by INVOKESPECIAL.
     *
     * @param target
     *            an expression specifying the target object.
     *            if null, "this" is the target.
     * @param classname
     *            the class name declaring the method.
     * @param methodname
     *            the method name.
     * @param descriptor
     *            the method descriptor.
     */
    public void recordSpecialProceed(
                    String target,
                    final String classname,
                    final String methodname,
                    final String descriptor,
                    final int methodIndex) throws CompileError{
        Parser p = new Parser(new Lex(target));
        final ASTree texpr = p.parseExpression(stable);

        ProceedHandler h = new ProceedHandler(){

            @Override
            public void doit(JvstCodeGen gen,Bytecode b,ASTList args) throws CompileError{
                gen.compileInvokeSpecial(texpr, methodIndex, descriptor, args);
            }

            @Override
            public void setReturnType(JvstTypeChecker c,ASTList args) throws CompileError{
                c.compileInvokeSpecial(texpr, classname, methodname, descriptor, args);
            }

        };

        gen.setProceedHandler(h, proceedName);
    }

    /**
     * Prepares to use $proceed().
     */
    public void recordProceed(ProceedHandler h){
        gen.setProceedHandler(h, proceedName);
    }

    /**
     * Compiles a statement (or a block).
     * <code>recordParams()</code> must be called before invoking
     * this method.
     *
     * <p>
     * Local variables that are not declared
     * in the compiled source text might not be accessible within that
     * source text. Fields and method parameters ($0, $1, ..) are available.
     */
    public void compileStmnt(String src) throws CompileError{
        Parser p = new Parser(new Lex(src));
        SymbolTable stb = new SymbolTable(stable);
        while (p.hasMore()){
            Stmnt s = p.parseStatement(stb);
            if (s != null){
                s.accept(gen);
            }
        }
    }

    /**
     * Compiles an exression. <code>recordParams()</code> must be
     * called before invoking this method.
     *
     * <p>
     * Local variables are not accessible
     * within the compiled source text. Fields and method parameters
     * ($0, $1, ..) are available if <code>recordParams()</code>
     * have been invoked.
     */
    public void compileExpr(String src) throws CompileError{
        ASTree e = parseExpr(src, stable);
        compileExpr(e);
    }

    /**
     * Parsers an expression.
     */
    public static ASTree parseExpr(String src,SymbolTable st) throws CompileError{
        Parser p = new Parser(new Lex(src));
        return p.parseExpression(st);
    }

    /**
     * Compiles an exression. <code>recordParams()</code> must be
     * called before invoking this method.
     *
     * <p>
     * Local variables are not accessible
     * within the compiled source text. Fields and method parameters
     * ($0, $1, ..) are available if <code>recordParams()</code>
     * have been invoked.
     */
    public void compileExpr(ASTree e) throws CompileError{
        if (e != null){
            gen.compileExpr(e);
        }
    }
}
